﻿#region License Information

/* •——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————•
   | Copyright Aptillon, Inc.                                                                                        |
   | This file is part of the CKS:API project - cksapi.codeplex.com                                                       |
   | Usage, modification and distribution subject to the license terms available here: http://cksapi.codeplex.com/license |
   |                                                                                                                      |
   | Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,             |
   | either express or implied.                                                                                           |
   |                                                                                                                      |
   | THIS MESSAGE MAY NOT BE LEGALLY REMOVED                                                                              |
   •——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————• */

#endregion

using System;
using CKS.API.Utilities;
using CKS.API.Contracts;
using CKS.API.Exceptions;
using Microsoft.SharePoint;
using System.Web.UI;
using System.Web;
using System.Diagnostics;
using System.Reflection;
using CKS.API.Extensions;
using System.Text;
using System.Collections;
using System.Collections.Generic;

namespace CKS.API.Logging
{

    public abstract class CKSLogger
    {

        #region Fields

        private readonly CodeContract _contract;

        protected bool shouldDispose = true;

        private Hashtable timers = new Hashtable();

        #endregion

        #region Structs

        private struct TimerData
        {
            internal string DescriptiveName;
            internal Stopwatch Watch;
        }

        #endregion

        #region Constructors

        public CKSLogger(SPWeb rootWeb, bool dispose)
        {
            if (Check.ThatIsNull(rootWeb))
            {
                rootWeb = SPContext.Current.Web;
                shouldDispose = false;
            }
            else
            {
                CurrentWeb = rootWeb;
                shouldDispose = dispose;
            }

            _contract = new CodeContract(this);


            /*
             * TODO: The following line is throwing an error when adding my Farm test web part...why?  Temporarily removed...
            Contract.RequiresThat(CodeContract.ValidateIsNotNull(RootWeb));
            */
        }

        public CKSLogger(SPWeb rootWeb) : this(rootWeb, true) { }

        public CKSLogger() { }

        #endregion

        #region Properties

        protected CodeContract Contract
        {
            get
            {
                return _contract;
            }
        }

        public SPWeb CurrentWeb { get; set; }

        #endregion

        #region Public Methods

        public abstract void BeginScope();

        public virtual void CatchUnhandledExceptions()
        {
            //can't do anything if we don't have context and a Page...
            if (Check.ThatIsNotNull(HttpContext.Current) && Check.That(HttpContext.Current.CurrentHandler.ValidateIsType<Page>()))
            {
                Page throwingPage = HttpContext.Current.CurrentHandler as Page;
                if (Check.ThatIsNotNull(throwingPage))
                {
                    //Register handler
                    throwingPage.Error += HandleUncaughtError;
                }
            }

        }


        public void DumpVariablesInRelease(List<CKSVariableWrapper> variables)
        {
            variables.ForEach(oneVariable =>
            {
                if (Check.ThatIsNull(oneVariable.Variable))
                {
                    Log(string.Format("Variable Dump.  Variable= {0}, Value= NULL", oneVariable.VariableName), CKSLoggingCategory.DebugInfo, LoggingSeverity.Information);
                    return;
                }
                if (null == oneVariable.PropertiesToDump || oneVariable.PropertiesToDump.Count == 0)
                {
                    Log(string.Format("Variable Dump.  Variable= {0}, Type = {1}, Value= {2}", oneVariable.VariableName, oneVariable.Variable.GetType(), oneVariable.Variable),
                        CKSLoggingCategory.DebugInfo, LoggingSeverity.Information);
                }
                else
                {
                    oneVariable.PropertiesToDump.ForEach(propertyName =>
                    {
                        object propertyValue = string.Empty;

                        PropertyInfo sourcePropertyInfo = oneVariable.Variable.GetType().GetProperty(propertyName);
                        if (null != sourcePropertyInfo)
                        {
                            propertyValue = sourcePropertyInfo.GetValue(oneVariable.Variable, null);

                            Log(string.Format("Variable Dump.  Variable= {0}, Property= {1}, Type = {2}, Value= {3}.  ",
                                oneVariable.VariableName,
                                propertyName,
                                sourcePropertyInfo.PropertyType.FullName,
                                (Check.ThatIsNotNull(propertyValue) ? propertyValue.ToString() : "Null")), CKSLoggingCategory.DebugInfo, LoggingSeverity.Information);
                        }
                        else
                        {
                            Log(string.Format("Unable to retrieve variable property value for property \"{0}\" of variable \"{1}\".", propertyName, oneVariable.VariableName), CKSLoggingCategory.DebugInfo, LoggingSeverity.Information);
                        }
                    });
                }
            });
        }

        [Conditional("DEBUG")]
        public void DumpVariablesInDebug(List<CKSVariableWrapper> variables)
        {
            DumpVariablesInRelease(variables);
        }

        [Conditional("DEBUG")]
        public void DumpOneVariableInDebug(string variableName, object variableValue)
        {
            DumpVariablesInRelease(new List<CKSVariableWrapper>()
            {
                new CKSVariableWrapper(variableName, variableValue)
            });
        }

        public void DumpOneVariableInRelease(string variableName, object variableValue)
        {
            DumpVariablesInRelease(new List<CKSVariableWrapper>()
            {
                new CKSVariableWrapper(variableName, variableValue)
            });
        }

        public abstract void EndScope();

        public virtual void HandleException(CKSExceptionDetails details)
        {
            details.Exception.Process(details, this);
        }

        public void HandleUncaughtError(object sender, EventArgs e)
        {
            try
            {
                if (Check.ThatIsNotNull(HttpContext.Current))
                {
                    Exception ex = HttpContext.Current.Server.GetLastError();
                    if (Check.ThatIsNotNull(ex))
                    {
                        string logMessage = String.Format("An unhandled error has occurred: {0} --- {1}", ex.Message, ex.StackTrace);

                        HandleException(new CKSExceptionDetails(ex, CKSLoggingCategory.Unhandled)
                        {
                            Display = ErrorDisplayType.None,
                            ThrowingControl = (sender as Control),
                            Severity = LoggingSeverity.CriticalError     //  <-- Switch to a Critical error level
                        });
                        HttpContext.Current.Server.ClearError();     // clear the error so it doesn't bubble up to the UI
                        EndScope();     // Flush the log

                        string redirectPage = String.Format("{0}/SiteAssets/CKS/{1}", SPContext.Current.Site.RootWeb.ServerRelativeUrl.TrimEnd('/'), Constants.ErrorPageName);

                        //redirect the user to the error page
                        if (CKSApiUtilities.IsInSandbox)
                        {
                            //TODO: Need to use JS redirect in a sandbox solution, but how to get the JS onto the page?
                        }
                        else
                        {
                            HttpContext.Current.Response.Redirect(redirectPage, true);
                        }
                    }
                }
            }
            catch
            {
                //Swallow error.  Normally we shouldn't do this, but we need to avoid an endless loop...
            }
        }

        public void InitializeSiteCollectionEnvironment()
        {

            try
            {

                CreateSiteAssets();
                InitializeSpecificEnvironment();
            }
            catch (Exception ex)
            {
                HandleException(new CKSExceptionDetails(ex, CKSLoggingCategory.Setup)
                {
                    Display = ErrorDisplayType.None,
                    ThrowingControl = null
                });

            }

        }

        public void UninitializeSiteCollectionEnvironment()
        {
            //TODO: Any necessary cleanup
        }

        public virtual void Write(string logMessage)
        {
            Write(logMessage, CKSLoggingCategory.General, LoggingSeverity.Information);
        }

        public virtual void Write(string logMessage, CKSLoggingCategory category, LoggingSeverity severity)
        {
            Log(logMessage, category, severity);

        }

        [Conditional("DEBUG")]
        public void WriteDebug(string logMessage)
        {
            Log(logMessage, CKSLoggingCategory.Miscellaneous, LoggingSeverity.Information);
        }


        [Conditional("DEBUG")]
        public virtual void WriteDebugMethodEnd()
        {
            StackTrace stackTrace = new StackTrace();
            MethodBase Method = stackTrace.GetFrame(1).GetMethod();
            string logMessage = string.Format("Method {0} finished at {1}, {2}.  ",
                                           Method.Name,
                                           DateTime.Now.ToShortDateString(),
                                           DateTime.Now.ToLongTimeString());
            WriteDebug(logMessage);
        }


        [Conditional("DEBUG")]
        public virtual void WriteDebugMethodStart()
        {
            StackTrace stackTrace = new StackTrace();
            MethodBase Method = stackTrace.GetFrame(1).GetMethod();
            string logMessage = string.Format("Method {0} called at {1}, {2}.  ",
                                           Method.Name,
                                           DateTime.Now.ToShortDateString(),
                                           DateTime.Now.ToLongTimeString());
            WriteDebug(logMessage);
        }

        public Guid WriteElapsedTimeStart(string descriptiveName)
        {
            Stopwatch watch = new Stopwatch();
            Guid id = Guid.NewGuid();
            watch.Start();

            timers.Add(id, new TimerData()
            {
                DescriptiveName = descriptiveName,
                Watch = watch

            });

            return id;
        }

        public void WriteElapsedTimeStop(Guid id)
        {
            if (id != Guid.Empty)
            {
                TimerData data = (TimerData)timers[id];

                Stopwatch watch = data.Watch;
                watch.Stop();

                Log(string.Format("Elapsed Time for {0} = {1} milliseconds", data.DescriptiveName, watch.ElapsedMilliseconds), CKSLoggingCategory.ElapsedTime, LoggingSeverity.Timing);
                watch = null;
                timers.Remove(id);
            }

        }

        #endregion

        #region Protected Methods

        protected internal abstract void InitializeSpecificEnvironment();

        protected internal abstract void Log(string message, CKSLoggingCategory category, LoggingSeverity severity);

        protected internal abstract void NotifyAdministrator(string alertId, string alertBody);

        protected internal abstract void WriteToDeveloperDashboard(string message, Action action);

        protected internal abstract void WriteToDeveloperDashboard(string message);

        protected internal virtual void WriteToDeveloperDashboard(string message, Action action, uint maxExecutionTime) { }

        #endregion

        #region Private Methods

        private void CreateSiteAssets()
        {
            Contract.RequiresThat(CodeContract.ValidateIsNotNull(CurrentWeb));

            SPDocumentLibrary siteAssets = CurrentWeb.Lists.TryGetList(Constants.SiteAssetsLibraryName) as SPDocumentLibrary;
            if (Check.That(CodeContract.ValidateIsNotNull(siteAssets)))
            {
                bool unsafeUpdates = CurrentWeb.AllowUnsafeUpdates;
                try
                {
                    CurrentWeb.AllowUnsafeUpdates = true;

                    //create "CKS" folder if necessary
                    SPFolder CKSFolder = siteAssets.RootFolder.TryGetFolder(Constants.SiteAssetsFolderName);

                    if (Check.ThatIsNull(CKSFolder))
                    {
                        CKSFolder = siteAssets.RootFolder.SubFolders.Add(Constants.SiteAssetsFolderName);
                    }

                    //create error page
                    SPFile errPage = CKSFolder.Files.Add(Constants.ErrorPageName, GetDefaultErrorPageContents(), true);
                    errPage.Update();

                    //create JS libraries
                    SPFile jsPage = CKSFolder.Files.Add(Constants.JSLibraryName, GetJSLibContents(), true);
                    jsPage.Update();
                }
                finally
                {
                    CurrentWeb.AllowUnsafeUpdates = unsafeUpdates;
                }
            }
        }

        private static void CreateUnhandledExceptionsList(SPWeb rootWeb)
        {
            object locker = new object();
            if (Check.ThatIsNotNull(rootWeb))
            {
                SPList logList = rootWeb.Lists.TryGetList(Constants.UnhandledExceptionListName);
                if (Check.ThatIsNull(logList))
                {
                    lock (locker)
                    {
                        logList = rootWeb.Lists.TryGetList(Constants.UnhandledExceptionListName);
                        if (Check.ThatIsNull(logList))
                        {

                            logList = rootWeb.Lists.GetList(rootWeb.Lists.Add(Constants.UnhandledExceptionListName, Constants.UnhandledExceptionListDescription, SPListTemplateType.GenericList), false);
                            SPFieldCollection fields = logList.Fields;
                            fields.Add(Constants.LogFieldName, SPFieldType.Note, true);
                            SPFieldMultiLineText logField = fields.GetFieldByInternalName(Constants.LogFieldName) as SPFieldMultiLineText;

                            SPView defaultView = logList.DefaultView;
                            SPViewFieldCollection viewFields = defaultView.ViewFields;
                            viewFields.Add(logField);
                            defaultView.Update();

                            logList.EnableVersioning = true;
                            logList.Hidden = true;
                            logList.OnQuickLaunch = false;
                            logList.Update();

                        }
                    }
                }
            }
        }

        private static byte[] GetDefaultErrorPageContents()
        {

            UTF8Encoding encoding = new UTF8Encoding();
            return encoding.GetBytes(Resources.Error);

        }

        private static byte[] GetJSLibContents()
        {

            UTF8Encoding encoding = new UTF8Encoding();
            return encoding.GetBytes(Resources.CKSErrorDisplay);

        }

        #endregion

        //Documentation:
        //Finalizers prohibited in O365
        //~CKSLogger()
        //{
        //    if ((Check.ThatAllAreNotNull(RootWeb, SPContext.Current)) && (shouldDispose))
        //    {
        //        RootWeb.Dispose();
        //    }
        //}

    }
}
